<?php

/**
 * http协程网络请求
 */

namespace Lib\Base;

class HttpRequest
{

    private $m_handle;
    private $m_host;
    private $m_port;
    private $m_path;
    private $m_query;
    private $m_path_query;
    private $options;

    public const HTTP_SUCCESS = 0;
    public const HTTP_URL_INVALID = -201;
    public const HTTP_CONNECT_ERROR = -202;
    public const HTTP_TIMEOUT = 28;
    public const HTTP_FAIL = -204;

    /**
     * http请求构造函数
     *
     * HttpCoRequest constructor.
     * @param array $options
     */
    public function __construct($options = ['decode' => true])
    {
        $this->m_handle = null;
        $this->options = $options;
    }

    /**
     * http请求析构函数
     */
    public function __destruct()
    {
        if ($this->m_handle != null) {
            $this->m_handle->close();
            $this->m_handle = null;
            unset($this->m_handle);
        }
    }

    /**
     * 设置配置
     *
     * @param $header
     */
    private function setOptions($header)
    {
        if (!isset($header['Host'])) {
            $header['Host'] = $this->m_host;
        }
        if (!isset($header['Accept'])) {
            $header['Accept'] = 'text/html,application/xhtml+xml,application/xml';
        }
        $this->m_handle->setHeaders($header);

        if (isset($this->options['cookie']) && $this->options['cookie']) {
            $this->m_handle->setCookies(Request::cookieParam());
        }
    }

    /**
     * @function connect($ip, $port = 80, $ssl = false)
     * @brief http连接
     * @param $ip string 连接的host地址
     * @param $port string 连接的port地址
     * @param $ssl bool 是否走https连接
     * @return 0 成功 -202 连接失败
     */
    private function connect($host, $port = 80, $ssl = false)
    {
        if ($this->m_handle == null) {

            if ($port == 443) {
                $ssl = true;
            }

            $this->m_handle = new \Swoole\Coroutine\Http\Client($host, $port, $ssl);

            if (!is_object($this->m_handle)) {
                return self::HTTP_CONNECT_ERROR;
            }
        }

        return self::HTTP_SUCCESS;
    }

    /**
     * 发起get请求
     *
     * @param string $url 请求url，如果有get参数，直接拼接在url后面
     * @param array $header http请求头部信息
     * @return array ['ret'=>'成功：0，失败：错误码  -201 url格式非法 -202 连接失败 -203 get超时 -204 get失败 ', 'msg'=>'具体的错误信息', data=>'请求数据。如果返回结果是json，并且设置的decode=true,返回数据，否则返回结果集字符串']
     */

    public function get($url, $header = array())
    {
        $rtnData = [];
        $extRtnData = [];

        //日志记录
        recordLog("http url = " . $url . ', header = ' . var_export($header, true), __FILE__, __LINE__, LP_INFO);

        //开始时间
        $startTime = microtime(true);

        if (isSwoole()) {

            //判断url格式是否符合标准
            if (!filter_var($url, FILTER_VALIDATE_URL)) {
                return ['ret' => self::HTTP_URL_INVALID, 'msg' => 'url格式错误', 'data' => []];
            }

            //解析url
            $this->parseUrl($url);

            //连接
            $ret = $this->connect($this->m_host, $this->m_port);

            if (isset($this->options['http_code']) && $this->options['http_code'] == 1) {
                $extRtnData['http_code'] = $this->m_handle->statusCode;
            }

            if ($ret != self::HTTP_SUCCESS) {
                $rtnData = ['ret' => $ret, 'msg' => '连接失败', 'data' => []];
                return empty($extRtnData) ? $rtnData : array_merge($rtnData, $extRtnData);
            }

            //设置头
            $this->setOptions($header);

            //设置超时时间
            $timeout = $this->options['timeout'] ?? 3;
            $this->m_handle->set(['timeout' => $timeout]);

            //请求
            $ret = $this->m_handle->get($this->m_path_query);

            if (isset($this->options['http_code']) && $this->options['http_code'] == 1) {
                $extRtnData['http_code'] = $this->m_handle->statusCode;
            }

            //结束时间
            $endTime = microtime(true);

            //耗时
            $costTime = round((($endTime - $startTime) * 1000), 3);

            recordLog("http get ret = " . $ret . ", errCode = " . $this->m_handle->errCode . ", costTime = " . $costTime, __FILE__, __LINE__, LP_INFO);

            if (!$ret) {
                if ($this->m_handle->errCode == 110) {
                    $rtnData = ['ret' => self::HTTP_TIMEOUT, 'msg' => '连接超时', 'data' => []];
                } else {
                    $rtnData = ['ret' => self::HTTP_FAIL, 'msg' => '请求失败', 'data' => []];
                }
                return empty($extRtnData) ? $rtnData : array_merge($rtnData, $extRtnData);
            }

            //状态码不等于200
            if ($this->m_handle->statusCode != 200) {
                $rtnData = ['ret' => self::HTTP_FAIL, 'msg' => "请求失败: " . $this->m_handle->statusCode, 'data' => []];
            } else {
                $rtnData = $this->parseResult($this->m_handle->body);
            }
            return empty($extRtnData) ? $rtnData : array_merge($rtnData, $extRtnData);
        } else {

            $options = $this->getBaseOptions($url, $header);
            $rHdCurl = curl_init();
            $ret = curl_setopt_array($rHdCurl, $options);
            if ($ret === false) {
                $errno = curl_errno($rHdCurl);
                $errmsg = curl_error($rHdCurl);
                return ['ret' => $errno, 'msg' => $errmsg, 'data' => []];
            }

            $result = $this->exceCurl($rHdCurl, $startTime);
            curl_close($rHdCurl);
            return $result;
        }
    }

    /**
     * 发起post请求
     *
     * @param string $url 请求url，如果有get参数，直接拼接在url后面
     * @param string|array 请求数据。这个参数可以是 urlencoded 后的字符串，类似'para1=val1&para2=val2&...'，也可以使用一个以字段名为键值，字段数据为值的数组。
     * @param array $header http请求头部信息
     * @return array ['ret'=>'成功：0，失败：错误码 -201 url格式非法 -204 post失败 -203 post超时', 'msg'=>'具体的错误信息', data=>'请求数据。如果返回结果是json，并且设置的decode=true,返回数据，否则返回结果集字符串']
     */
    public function post($url, $postData, $header = array())
    {
        $rtnData = [];
        $extRtnData = [];

        //日志记录
        recordLog("http post: url = " . $url . ', header = ' . var_export($header, true) . ', $postData = ' . var_export($postData, true), __FILE__, __LINE__, LP_INFO);

        //开始时间
        $startTime = microtime(true);

        if (isSwoole()) {

            if (!filter_var($url, FILTER_VALIDATE_URL)) {
                return ['ret' => self::HTTP_URL_INVALID, 'msg' => 'url格式错误', 'data' => []];
            }

            //解析url
            $this->parseUrl($url);

            //连接
            $ret = $this->connect($this->m_host, $this->m_port);

            if (isset($this->options['http_code']) && $this->options['http_code'] == 1) {
                $extRtnData['http_code'] = $this->m_handle->statusCode;
            }

            if ($ret != self::HTTP_SUCCESS) {
                $rtnData = ['ret' => $ret, 'msg' => '连接失败', 'data' => []];
                return empty($extRtnData) ? $rtnData : array_merge($rtnData, $extRtnData);
            }


            //设置头
            $this->setOptions($header);

            //设置超时时间
            $timeout = $this->options['timeout'] ?? 3;
            $this->m_handle->set(['timeout' => $timeout]);

            $ret = $this->m_handle->post($this->m_path_query, $postData);

            if (isset($this->options['http_code']) && $this->options['http_code'] == 1) {
                $extRtnData['http_code'] = $this->m_handle->statusCode;
            }

            //结束时间
            $endTime = microtime(true);

            //耗时
            $costTime = round((($endTime - $startTime) * 1000), 3);

            recordLog("http post ret = " . $ret . ", errCode = " . $this->m_handle->errCode . ", costTime = " . $costTime, __FILE__, __LINE__, LP_INFO);

            if (!$ret) {
                if ($this->m_handle->errCode == 110) {
                    $rtnData = ['ret' => self::HTTP_TIMEOUT, 'msg' => '连接超时', 'data' => []];
                } else {
                    $rtnData = ['ret' => self::HTTP_FAIL, 'msg' => '请求失败', 'data' => []];
                }
                return empty($extRtnData) ? $rtnData : array_merge($rtnData, $extRtnData);
            }

            //状态码不等于200
            if ($this->m_handle->statusCode != 200) {
                $rtnData =  ['ret' => self::HTTP_FAIL, 'msg' => "请求失败: " . $this->m_handle->statusCode, 'data' => []];
            } else {
                $rtnData = $this->parseResult($this->m_handle->body);
            }
            return empty($extRtnData) ? $rtnData : array_merge($rtnData, $extRtnData);
        } else {

            $options = $this->getBaseOptions($url, $header);

            //swoole 100 continue
            //非swoole server 加上 Expect: 会导致 404
            //$options[CURLOPT_HTTPHEADER] = empty($options[CURLOPT_HTTPHEADER]) ? ['Expect:'] : \array_merge($options[CURLOPT_HTTPHEADER], ['Expect:']);
            $options[CURLOPT_POST] = true;
            $options[CURLOPT_POSTFIELDS] = $postData;

            $rHdCurl = curl_init();
            $ret = curl_setopt_array($rHdCurl, $options);
            if ($ret === false) {
                $errno = curl_errno($rHdCurl);
                $errmsg = curl_error($rHdCurl);
                return ['ret' => $errno, 'msg' => $errmsg, 'data' => []];
            }

            $result = $this->exceCurl($rHdCurl, $startTime);
            curl_close($rHdCurl);
            return $result;
        }
    }

    /**
     * 解析url
     *
     * @param $url
     */
    private function parseUrl($url)
    {
        $arr = parse_url($url);
        $this->m_host = $arr['host'];
        if (isset($arr['scheme']) && $arr['scheme'] == 'https') {
            $this->m_port = isset($arr['port']) ? $arr['port'] : 443;
        } else {
            $this->m_port = isset($arr['port']) ? $arr['port'] : 80;
        }
        $this->m_path = isset($arr['path']) ? $arr['path'] : '/';
        if (array_key_exists("query", $arr)) {
            $this->m_query = $arr['query'];
            $this->m_path_query = $this->m_path . '?' . $this->m_query;
        } else {
            $this->m_query = "";
            $this->m_path_query = $this->m_path;
        }
    }

    /**
     * 构造curl_setopt选项数组
     * @param string $url 请求url
     * @param array $header 请求头部信息
     * @return array
     */
    private function getBaseOptions($url, $header = [])
    {
        $options = [
            CURLOPT_URL => $url,
            CURLOPT_CONNECTTIMEOUT => isset($this->options['connectTimeout']) ? $this->options['connectTimeout'] : 1,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => isset($this->options['timeout']) ? $this->options['timeout'] : 3,
            CURLOPT_HEADER => isset($this->options['header']) ? $this->options['header'] : false,
        ];

        //如果是https，取消验证
        if (strpos($url, 'https://') === 0) {
            $options[CURLOPT_SSL_VERIFYPEER] = false;
            $options[CURLOPT_SSL_VERIFYHOST] = false;
        }

        if (is_array($header) && !empty($header)) {
            $options[CURLOPT_HTTPHEADER] = $header;
        }

        if (isset($this->options['cookie']) && $this->options['cookie']) {
            $cookieStr = Common::cookie2Str();
            if ($cookieStr != '') {
                $options[CURLOPT_COOKIESESSION] = true;
                $options[CURLOPT_COOKIE] = $cookieStr;
            }
        }
        return $options;
    }

    /**
     * 执行curl
     * @param resource $rHdCurl curl_init()函数返回的句柄
     *
     */
    private function exceCurl($rHdCurl, $startTime)
    {
        $rtnData = [];
        $extRtnData = [];
        $result = curl_exec($rHdCurl);
        if (isset($this->options['http_code']) && $this->options['http_code'] == 1) {
            $extRtnData['http_code'] = curl_getinfo($rHdCurl, CURLINFO_HTTP_CODE);
        }
        recordLog(__METHOD__ . '|options=' . var_export($this->options, true), __FILE__, __LINE__, LP_INFO);
        //结束时间
        $endTime = microtime(true);

        //耗时
        $costTime = round((($endTime - $startTime) * 1000), 3);

        recordLog("http , costTime = " . $costTime . ", return = " . $result, __FILE__, __LINE__, LP_INFO);

        if ($result === false) {
            $errno = curl_errno($rHdCurl);
            $errmsg = curl_error($rHdCurl);
            $rtnData =  ['ret' => $errno, 'msg' => $errmsg, 'data' => []];
        } else {
            //返回值进行编码转换
            if (isset($this->options['convert']) && count($this->options['convert']) == 2) {
                $result = Common::charsetConvert($result, $this->options['convert'][0], $this->options['convert'][1]);
            }

            if (isset($this->options['decode']) && $this->options['decode']) {
                $data = json_decode($result, true);
                if (is_null($data)) {
                    $rtnData = ['ret' => json_last_error(), 'msg' => json_last_error_msg(), 'data' => []];
                } else {
                    $rtnData = ['ret' => 0, 'msg' => '', 'data' => $data];
                }
            } else {
                $rtnData = ['ret' => 0, 'msg' => '', 'data' => $result];
            }
        }

        recordLog(__METHOD__ . '|extRtnData=' . var_export($extRtnData, true), __FILE__, __LINE__, LP_INFO);

        if (!empty($extRtnData)) {
            $rtnData = array_merge($rtnData, $extRtnData);
        }
        return $rtnData;
    }

    /**
     * 解析结果
     *
     * @param $result
     * @return array
     */
    private function parseResult($result)
    {
        //日志记录
        recordLog("http return = " . $result, __FILE__, __LINE__, LP_INFO);

        //返回值进行编码转换
        if (isset($this->options['convert']) && count($this->options['convert']) == 2) {
            $result = Common::charsetConvert($result, $this->options['convert'][0], $this->options['convert'][1]);
        }

        if (isset($this->options['decode']) && $this->options['decode']) {
            $data = json_decode($result, true);
            if (is_null($data)) {
                return ['ret' => json_last_error(), 'msg' => json_last_error_msg(), 'data' => []];
            } else {
                return ['ret' => 0, 'msg' => '', 'data' => $data];
            }
        } else {
            return ['ret' => 0, 'msg' => '', 'data' => $result];
        }
    }
}
